#include "MyGlWindow.h"
#include "camera.h"
#include <QOpenGLExtraFunctions>
#include <QOpenGLShaderProgram>
#include <QOpenGLVertexArrayObject>
#include <QOpenGLBuffer>
#include <QOpenGLFunctions_3_3_Core>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QDebug>



static GLfloat  vertices[] = {
    // positions            // color
    0.5f,  0.5f,  0.5f,     1.0f, 0.0f, 0.0f,       // front top right			0
   -0.5f,  0.5f,  0.5f,     0.0f, 1.0f, 0.0f,       // front top left			1
   -0.5f, -0.5f,  0.5f,     0.0f, 0.0f, 1.0f,       // front buttom left		2
    0.5f, -0.5f,  0.5f,     0.0f, 0.0f, 0.0f,       // front buttom right		3
    0.5f,  0.5f, -0.5f,     1.0f, 1.0f, 0.0f,       // back top right			4
   -0.5f,  0.5f, -0.5f,     0.0f, 1.0f, 1.0f,       // back top left			5
   -0.5f, -0.5f, -0.5f,     1.0f, 0.0f, 1.0f,       // back buttom left			6
    0.5f, -0.5f, -0.5f,     1.0f, 1.0f, 1.0f        // back buttom right		7
};

static unsigned int indices[] = { // note that we start from 0!

    // front
    0, 1, 2,
    2, 3, 0,

    // back
    7, 6, 5,
    5, 4, 7,

    // top
    0, 4, 5,
    5, 1, 0,

    // buttom
    3, 2, 6,
    6, 7, 3,

    // left
    1, 5, 6,
    6, 2, 1,

    // right
    0, 3, 7,
    7, 4, 0
};


MyGlWindow::MyGlWindow()
    : MY_GL_FUNC(nullptr)
    , program(nullptr)
    , vao(nullptr)
    , vbo(nullptr)
    , ebo(nullptr)
    , trans(-1)
    , camera(nullptr)
    , firstMouse(true)
{}

void MyGlWindow::initializeGL()
{
    MY_GL_FUNC = context()->versionFunctions<QOpenGLFunctions_3_3_Core>();

    if (! MY_GL_FUNC) {
        qCritical("NO support for QOpenGLFunctions_3_3_Core!");
        ::exit(0);
    }

    connect(context(), SIGNAL(aboutToBeDestroyed()), SLOT(teardownGL()));
    MY_GL_FUNC->initializeOpenGLFunctions();
    MY_GL_FUNC->glEnable(GL_CULL_FACE);
    MY_GL_FUNC->glClearColor(0.0f, 0.0f, 0.0f, 1.0f);

    bool rc = false;

    // create vertex buffer objects (VBO)
    vbo = new QOpenGLBuffer;
    rc = vbo->create();
    Q_ASSERT(rc);

    rc = vbo->bind();
    Q_ASSERT(rc);

    //vbo->setUsagePattern(QOpenGLBuffer::StaticDraw);
    qDebug() << Q_FUNC_INFO << "VBO usage pattern" << vbo->usagePattern();
    vbo->allocate(vertices, sizeof(vertices));

    // create VAO
    vao = new QOpenGLVertexArrayObject(this);
    rc = vao->create();
    Q_ASSERT(rc);

    vao->bind();

    // create element buffer objects (EBO)
    ebo = new QOpenGLBuffer(QOpenGLBuffer::IndexBuffer);
    rc = ebo->create();
    Q_ASSERT(rc);

    rc = ebo->bind();
    Q_ASSERT(rc);

    //ebo->setUsagePattern(QOpenGLBuffer::StaticDraw);
    qDebug() << Q_FUNC_INFO << "EBO usage pattern" << ebo->usagePattern();
    ebo->allocate(indices, sizeof(indices));

    // create program
    program = new QOpenGLShaderProgram(this);

    rc = program->addShaderFromSourceFile(QOpenGLShader::Vertex, ":/shaders/simple.vert");
    Q_ASSERT(rc);

    rc = program->addShaderFromSourceFile(QOpenGLShader::Fragment, ":/shaders/simple.frag");
    Q_ASSERT(rc);

    rc = program->link();
    Q_ASSERT(rc);

    rc = program->bind();
    Q_ASSERT(rc);

    program->enableAttributeArray(0);
    program->enableAttributeArray(1);
    program->setAttributeBuffer(0, GL_FLOAT, 0, 3, 6 * sizeof(GLfloat));
    program->setAttributeBuffer(1, GL_FLOAT, 3 * sizeof(GLfloat), 3, 6 * sizeof(GLfloat));

    // Cache Uniform Locations
    trans = program->uniformLocation("trans");
    Q_ASSERT(trans >= 0);

    // Release (unbind) all
    program->release();

    // do NOT unbind the EBO while a VAO is active as the bound element buffer object IS stored in the VAO; keep the EBO bound.
    // ebo->release();

    vao->release();
    ebo->release();
    vbo->release();

    camera = new Camera(QVector3D(0.0f, 0.0f, 3.0f));
    printContextInformation();
}

void MyGlWindow::paintGL()
{
    MY_GL_FUNC->glClear(GL_COLOR_BUFFER_BIT);
    bool rc = false;

    rc = program->bind();
    Q_ASSERT(rc);

    QMatrix4x4  projection;
    projection.perspective(camera->fov, width() / float(height()), 0.1f, 100.0f);

    program->setUniformValue(trans, projection * camera->viewMatrix());

    vao->bind();
    MY_GL_FUNC->glDrawElements(GL_TRIANGLES, sizeof(indices) / sizeof(unsigned), GL_UNSIGNED_INT, nullptr);
    vao->release();
    program->release();
}

void MyGlWindow::resizeGL(int w, int h)
{
    Q_UNUSED(w)
    Q_UNUSED(h)

    firstMouse = true;
}

void MyGlWindow::keyPressEvent(QKeyEvent *ev)
{
    switch (ev->key()) {

    case Qt::Key_W:
        camera->move(Camera::FORWARD);
        update();
        break;

    case Qt::Key_S:
        camera->move(Camera::BACKWARD);
        update();
        break;

    case Qt::Key_A:
        camera->move(Camera::LEFT);
        update();
        break;

    case Qt::Key_D:
        camera->move(Camera::RIGHT);
        update();
        break;

    default:
        ev->ignore();
        break;
    }
}

void MyGlWindow::mouseMoveEvent(QMouseEvent *ev)
{
    if (firstMouse) {
        lastMousePos = ev->pos();
        firstMouse = false;
        return;
    }

    QPoint diff = ev->pos() - lastMousePos;

    if (QPoint::dotProduct(diff, diff) > 2 * 2) {
        // ignore the case in which the mouse move drastically
        firstMouse = true;
        return;
    }

    float xoffset = ev->x() - lastMousePos.x();
    float yoffset = lastMousePos.y() - ev->y();

    lastMousePos = ev->pos();

    camera->rotate(xoffset, yoffset);
    update();
}

void MyGlWindow::wheelEvent(QWheelEvent *ev)
{

    if (ev->delta() > 0) {

        camera->zoomIn();
        update();

    } else if (ev->delta() < 0) {

        camera->zoomOut();
        update();

    } else
        ev->ignore();
}

void MyGlWindow::teardownGL()
{
    delete camera;
    camera = nullptr;

    delete program;
    program = nullptr;

    delete vao;
    vao = nullptr;

    delete ebo;
    ebo = nullptr;

    delete vbo;
    vbo = nullptr;
}

void MyGlWindow::printContextInformation()
{
  QString glType;
  QString glVersion;
  QString glProfile;

  // Get Version Information
  glType = (context()->isOpenGLES()) ? "OpenGL ES" : "OpenGL";
  glVersion = reinterpret_cast<const char*>(MY_GL_FUNC->glGetString(GL_VERSION));
//  glVersion = reinterpret_cast<const char*>(glGetString(GL_VERSION));

  // Get Profile Information
#define CASE(c) case QSurfaceFormat::c: glProfile = #c; break
  switch (format().profile())
  {
    CASE(NoProfile);
    CASE(CoreProfile);
    CASE(CompatibilityProfile);
  }
#undef CASE

  // qPrintable() will print our QString w/o quotes around it.
  qDebug() << qPrintable(glType) << qPrintable(glVersion) << "(" << qPrintable(glProfile) << ")";
}
